/*
 * Copyright (C) 2016 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { Chain, Interceptor } from "../Interceptor"
import { RealInterceptorChain } from "../http/RealInterceptorChain"
import { OkHttpClient } from "../OkHttpClient"
import { Response } from "../Response"
import { List } from "@kit.ArkTS"
import { Exchange } from "../connection/Exchange"
import { Request } from '../Request'
import { RealCall } from "../connection/RealCall"
import {
  CertificateException,
  ConnectionShutdownException,
  FileNotFoundException,
  InterruptedIOException,
  IOException,
  ProtocolException,
  RouteException,
  SocketTimeoutException,
  SSLHandshakeException,
  SSLPeerUnverifiedException} from "../Error/error"
import { http } from "@kit.NetworkKit"
import { ProxyType } from "../Socket/Proxy"
import { ReportService } from "@ohos/hypium/src/main/service"
import { HttpMethod } from "./HttpMethod"
import { canReuseConnectionFor } from "../utils/Utils"

/**
 * This interceptor recovers from failures and follows redirects as necessary. It may throw an
 * [IOException] if the call was canceled.
 */
export class RetryAndFollowUpInterceptor implements Interceptor {
  private client: OkHttpClient
  constructor(client: OkHttpClient) {
    this.client = client
  }
  static MAX_FOLLOW_UPS = 20

  // @Throws(IOException::class)
  async intercept(chain: Chain): Promise<Response> {
    let realChain = chain as RealInterceptorChain
    let request = chain.request()
    let call = realChain.call() as RealCall
    let followUpCount = 0
    let priorResponse: Response = null
    let newExchangeFinder = true
    let recoveredFailures = new List<IOException>()
    while (true) {
      console.log("okhttp test log :: " , "RetryAndFollowUpInterceptor : enterNetworkInterceptorExchange")
      call.enterNetworkInterceptorExchange(request, newExchangeFinder)
      let response: Response
      let closeActiveExchange = true
      try {
        if (call.isCanceled()) {
          throw new IOException("Canceled")
        }
        try {
          console.log("okhttp test log :: " , "RetryAndFollowUpInterceptor : 上半段结束")
          response = await realChain.proceed(request)
          newExchangeFinder = true
        } catch (e) {
          if (e instanceof RouteException){
            if (!this.recover(e.lastConnectException, call, request, false)) {
              throw e.firstConnectException.withSuppressed(recoveredFailures)
            } else {
              recoveredFailures.add(e.firstConnectException)
            }
            newExchangeFinder = false
            continue
          }
          if (e instanceof IOException){
            // An attempt to communicate with a server failed. The request may have been sent.
            if (!this.recover(e, call, request, !(e instanceof ConnectionShutdownException))) {
              throw e.withSuppressed(recoveredFailures)
            } else {
              recoveredFailures.add(e)
            }
            newExchangeFinder = false
            continue
          }
          // 未知异常类型，重新抛出
          console.log("okhttp test log :: " , "RetryAndFollowUpInterceptor : unknown error, rethrowing:", e)
          throw e
        }

        // Attach the prior response if it exists. Such responses never have a body.
        if (priorResponse !== null && priorResponse !== undefined) {
          response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
              .body(null)
              .build())
            .build()
        }

        let exchange = call.interceptorScopedExchange
        let followUp = this.followUpRequest(response, exchange)

        if (followUp === null || followUp === undefined) {
          if (exchange !== null && exchange !== undefined && exchange._isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }

        let followUpBody = followUp.body
        if (followUpBody !== null && followUpBody !== undefined && followUpBody.isOneShot()) {
          closeActiveExchange = false
          return response
        }

        response.body?.close()

        if (++followUpCount > RetryAndFollowUpInterceptor.MAX_FOLLOW_UPS) {
          throw new ProtocolException("Too many follow-up requests: $followUpCount")
        }

        request = followUp
        priorResponse = response
      } finally {
        console.log("okhttp test log :: " , "RetryAndFollowUpInterceptor : 下半段结束")
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

  /**
   * Report and attempt to recover from a failure to communicate with a server. Returns true if
   * `e` is recoverable, or false if the failure is permanent. Requests with a body can only
   * be recovered if the body is buffered or if the failure occurred before the request has been
   * sent.
   */
  private recover(e: Error, call: RealCall, userRequest: Request, requestSendStarted: boolean): boolean {
    // The application layer has forbidden retries.
    if (!this.client.retryOnConnectionFailure) return false

    // We can't send the request body again.
    if (requestSendStarted && this.requestIsOneShot(e, userRequest)) return false

    // This exception is fatal.
    if (!this.isRecoverable(e, requestSendStarted)) return false

    // No more routes to attempt.
    if (!call.retryAfterFailure()) return false

    // For failure recovery, use the same route selector with a new connection.
    return true
  }

  private requestIsOneShot(e: Error, userRequest: Request): boolean {
    let requestBody = userRequest.body
    return (requestBody !== null && requestBody !== undefined && requestBody.isOneShot()) ||
      e instanceof  FileNotFoundException
  }

  private isRecoverable(e: Error, requestSendStarted: boolean): boolean {
    // If there was a protocol problem, don't recover.
    if (e instanceof ProtocolException) {
      return false
    }

    // If there was an interruption don't recover, but if there was a timeout connecting to a route
    // we should try the next route (if there is one).
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted
    }

    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
    // again with a different route.
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e instanceof CertificateException) {  //e.cause instanceof CertificateException
        return false
      }
    }
    if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      return false
    }
    // An example of one we might want to retry with a different route is a problem connecting to a
    // proxy and would manifest as a standard IOException. Unless it is one we know we should not
    // retry, we return true and try a new route.
    return true
  }

  /**
   * Figures out the HTTP request to make in response to receiving [userResponse]. This will
   * either add authentication headers, follow redirects or handle a client request timeout. If a
   * follow-up is either unnecessary or not applicable, this returns null.
   */
  // @Throws(IOException::class)
  private followUpRequest(userResponse: Response, exchange?: Exchange): Request | null {
    let route = exchange?.connection?.route()
    let responseCode = userResponse.code

    let method = userResponse.request.method
    if (responseCode === http.ResponseCode.PROXY_AUTH){
      let selectedProxy = route.proxy
      if (selectedProxy.proxyType !== ProxyType.HTTP){
        throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
      }
      return this.client.proxyAuthenticator.authenticate(route, userResponse)
    }
    if (responseCode === http.ResponseCode.UNAUTHORIZED){
      return this.client.authenticator.authenticate(route, userResponse)
    }
    if (responseCode === 307 ||
      responseCode === 308 ||
      responseCode === http.ResponseCode.MULT_CHOICE ||
      responseCode === http.ResponseCode.MOVED_PERM ||
      responseCode === http.ResponseCode.MOVED_TEMP ||
      responseCode === http.ResponseCode.SEE_OTHER){
      return this.buildRedirectRequest(userResponse, method)
    }
    if (responseCode === http.ResponseCode.CLIENT_TIMEOUT){
      // 408's are rare in practice, but some servers like HAProxy use this response code. The
      // spec says that we may repeat the request without modifications. Modern browsers also
      // repeat the request (even non-idempotent ones.)
      if (!this.client.retryOnConnectionFailure) {
        // The application layer has directed us not to retry the request.
        return null
      }
      let requestBody = userResponse.request.body
      if (requestBody !== null && requestBody !== undefined && requestBody.isOneShot()) {
        return null
      }
      let priorResponse = userResponse.priorResponse
      if (priorResponse !== null && priorResponse !== undefined && priorResponse.code == http.ResponseCode.CLIENT_TIMEOUT) {
        // We attempted to retry and got another timeout. Give up.
        return null
      }

      if (this.retryAfter(userResponse, 0) > 0) {
        return null
      }

      return userResponse.request
    }
    if (responseCode === http.ResponseCode.UNAVAILABLE) {
      let priorResponse = userResponse.priorResponse
      if (priorResponse !== null && priorResponse !== undefined && priorResponse.code == http.ResponseCode.UNAVAILABLE) {
        // We attempted to retry and got another timeout. Give up.
        return null
      }
      if (this.retryAfter(userResponse, Number.MAX_VALUE) == 0) {
        // specifically received an instruction to retry without delay
        return userResponse.request
      }

      return null
    }
    if (responseCode === 421) {
      // OkHttp can coalesce HTTP/2 connections even if the domain names are different. See
      // RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then
      // we can retry on a different connection.
      let requestBody = userResponse.request.body
      if (requestBody !== null && requestBody !== undefined && requestBody.isOneShot()) {
        return null
      }

      if (exchange === null || exchange === undefined || !exchange.isCoalescedConnection) {
        return null
      }

      exchange.connection.noCoalescedConnections()
      return userResponse.request
    }

    return null
  }

  private buildRedirectRequest(userResponse: Response, method: string): Request| null {
    // Does the client allow redirects?
    if (!this.client.followRedirects) return null

    let location = userResponse.header("Location")
    if (location === null){
      return null
    }
    // Don't follow redirects to unsupported protocols.
    let url = userResponse.request.url.resolve(location)
    if (url === null){
      return null
    }

    // If configured, don't follow redirects between SSL and non-SSL.
    let sameScheme = url.scheme === userResponse.request.url.scheme
    if (!sameScheme && !this.client.followSslRedirects) return null

    // Most redirects don't include a request body.
    let requestBuilder = userResponse.request.newBuilder()
    if (HttpMethod.permitsRequestBody(method)) {
      let responseCode = userResponse.code
      let maintainBody = HttpMethod.redirectsWithBody(method) || responseCode === 308 || responseCode === 307
      if (HttpMethod.redirectsToGet(method) && responseCode !== 308 && responseCode !== 307) {
        requestBuilder.method("GET", null)
      } else {
        let requestBody = maintainBody ? userResponse.request.body : null
        requestBuilder.method(method, requestBody)
      }
      if (!maintainBody) {
        requestBuilder.removeHeader("Transfer-Encoding")
        requestBuilder.removeHeader("Content-Length")
        requestBuilder.removeHeader("Content-Type")
      }
    }

    // When redirecting across hosts, drop all authentication headers. This
    // is potentially annoying to the application layer since they have no
    // way to retain them.
    if (!canReuseConnectionFor(userResponse.request.url, url)) {
      requestBuilder.removeHeader("Authorization")
    }

    return requestBuilder.url(url).build()
  }

  private retryAfter(userResponse: Response, defaultDelay: number): number {
    let header = userResponse.header("Retry-After")
    if (header === null || header === undefined){
      return defaultDelay
    }

    // https://tools.ietf.org/html/rfc7231#section-7.1.3
    // currently ignores a HTTP-date, and assumes any non int 0 is a delay
    if (header.match(/^\d+$/)) {
      return Number(header).valueOf()
    }
    return Number.MAX_VALUE
  }
}
